/**
 * @license
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import {GrDiffLine} from './gr-diff-line.js';

/**
 * A chunk of the diff that should be rendered together.
 *
 * @constructor
 * @param {!GrDiffGroup.Type} type
 * @param {!Array<!GrDiffLine>=} opt_lines
 */
export function GrDiffGroup(type, opt_lines) {
  /** @type {!GrDiffGroup.Type} */
  this.type = type;

  /** @type {boolean} */
  this.dueToRebase = false;

  /**
   * True means all changes in this line are whitespace changes that should
   * not be highlighted as changed as per the user settings.
   *
   * @type{boolean}
   */
  this.ignoredWhitespaceOnly = false;

  /**
   * True means it should not be collapsed (because it was in the URL, or
   * there is a comment on that line)
   */
  this.keyLocation = false;

  /** @type {?HTMLElement} */
  this.element = null;

  /** @type {!Array<!GrDiffLine>} */
  this.lines = [];
  /** @type {!Array<!GrDiffLine>} */
  this.adds = [];
  /** @type {!Array<!GrDiffLine>} */
  this.removes = [];

  /** Both start and end line are inclusive. */
  this.lineRange = {
    left: {start: null, end: null},
    right: {start: null, end: null},
  };

  if (opt_lines) {
    opt_lines.forEach(this.addLine, this);
  }
}

/** @enum {string} */
GrDiffGroup.Type = {
  /** Unchanged context. */
  BOTH: 'both',

  /** A widget used to show more context. */
  CONTEXT_CONTROL: 'contextControl',

  /** Added, removed or modified chunk. */
  DELTA: 'delta',
};

/**
 * Hides lines in the given range behind a context control group.
 *
 * Groups that would be partially visible are split into their visible and
 * hidden parts, respectively.
 * The groups need to be "common groups", meaning they have to have either
 * originated from an `ab` chunk, or from an `a`+`b` chunk with
 * `common: true`.
 *
 * If the hidden range is 1 line or less, nothing is hidden and no context
 * control group is created.
 *
 * @param {!Array<!GrDiffGroup>} groups Common groups, ordered by their line
 *     ranges.
 * @param {number} hiddenStart The first element to be hidden, as a
 *     non-negative line number offset relative to the first group's start
 *     line, left and right respectively.
 * @param {number} hiddenEnd The first visible element after the hidden range,
 *     as a non-negative line number offset relative to the first group's
 *     start line, left and right respectively.
 * @return {!Array<!GrDiffGroup>}
 */
GrDiffGroup.hideInContextControl = function(groups, hiddenStart, hiddenEnd) {
  if (groups.length === 0) return [];
  // Clamp hiddenStart and hiddenEnd - inspired by e.g. substring
  hiddenStart = Math.max(hiddenStart, 0);
  hiddenEnd = Math.max(hiddenEnd, hiddenStart);

  let before = [];
  let hidden = groups;
  let after = [];

  const numHidden = hiddenEnd - hiddenStart;

  // Only collapse if there is more than 1 line to be hidden.
  if (numHidden > 1) {
    if (hiddenStart) {
      [before, hidden] = GrDiffGroup._splitCommonGroups(hidden, hiddenStart);
    }
    if (hiddenEnd) {
      [hidden, after] = GrDiffGroup._splitCommonGroups(
          hidden, hiddenEnd - hiddenStart);
    }
  } else {
    [hidden, after] = [[], hidden];
  }

  const result = [...before];
  if (hidden.length) {
    const ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
    ctxLine.contextGroups = hidden;
    const ctxGroup = new GrDiffGroup(
        GrDiffGroup.Type.CONTEXT_CONTROL, [ctxLine]);
    result.push(ctxGroup);
  }
  result.push(...after);
  return result;
};

/**
 * Splits a list of common groups into two lists of groups.
 *
 * Groups where all lines are before or all lines are after the split will be
 * retained as is and put into the first or second list respectively. Groups
 * with some lines before and some lines after the split will be split into
 * two groups, which will be put into the first and second list.
 *
 * @param {!Array<!GrDiffGroup>} groups
 * @param {number} split A line number offset relative to the first group's
 *     start line at which the groups should be split.
 * @return {!Array<!Array<!GrDiffGroup>>} The outer array has 2 elements, the
 *   list of groups before and the list of groups after the split.
 */
GrDiffGroup._splitCommonGroups = function(groups, split) {
  if (groups.length === 0) return [[], []];
  const leftSplit = groups[0].lineRange.left.start + split;
  const rightSplit = groups[0].lineRange.right.start + split;

  const beforeGroups = [];
  const afterGroups = [];
  for (const group of groups) {
    if (group.lineRange.left.end < leftSplit ||
        group.lineRange.right.end < rightSplit) {
      beforeGroups.push(group);
      continue;
    }
    if (leftSplit <= group.lineRange.left.start ||
        rightSplit <= group.lineRange.right.start) {
      afterGroups.push(group);
      continue;
    }

    const before = [];
    const after = [];
    for (const line of group.lines) {
      if ((line.beforeNumber && line.beforeNumber < leftSplit) ||
          (line.afterNumber && line.afterNumber < rightSplit)) {
        before.push(line);
      } else {
        after.push(line);
      }
    }

    if (before.length) {
      beforeGroups.push(before.length === group.lines.length ?
        group : group.cloneWithLines(before));
    }
    if (after.length) {
      afterGroups.push(after.length === group.lines.length ?
        group : group.cloneWithLines(after));
    }
  }
  return [beforeGroups, afterGroups];
};

/**
 * Creates a new group with the same properties but different lines.
 *
 * The element property is not copied, because the original element is still a
 * rendering of the old lines, so that would not make sense.
 *
 * @param {!Array<!GrDiffLine>} lines
 * @return {!GrDiffGroup}
 */
GrDiffGroup.prototype.cloneWithLines = function(lines) {
  const group = new GrDiffGroup(this.type, lines);
  group.dueToRebase = this.dueToRebase;
  group.ignoredWhitespaceOnly = this.ignoredWhitespaceOnly;
  return group;
};

/** @param {!GrDiffLine} line */
GrDiffGroup.prototype.addLine = function(line) {
  this.lines.push(line);

  const notDelta = (this.type === GrDiffGroup.Type.BOTH ||
      this.type === GrDiffGroup.Type.CONTEXT_CONTROL);
  if (notDelta && (line.type === GrDiffLine.Type.ADD ||
      line.type === GrDiffLine.Type.REMOVE)) {
    throw Error('Cannot add delta line to a non-delta group.');
  }

  if (line.type === GrDiffLine.Type.ADD) {
    this.adds.push(line);
  } else if (line.type === GrDiffLine.Type.REMOVE) {
    this.removes.push(line);
  }
  this._updateRange(line);
};

/** @return {!Array<{left: GrDiffLine, right: GrDiffLine}>} */
GrDiffGroup.prototype.getSideBySidePairs = function() {
  if (this.type === GrDiffGroup.Type.BOTH ||
      this.type === GrDiffGroup.Type.CONTEXT_CONTROL) {
    return this.lines.map(line => {
      return {
        left: line,
        right: line,
      };
    });
  }

  const pairs = [];
  let i = 0;
  let j = 0;
  while (i < this.removes.length || j < this.adds.length) {
    pairs.push({
      left: this.removes[i] || GrDiffLine.BLANK_LINE,
      right: this.adds[j] || GrDiffLine.BLANK_LINE,
    });
    i++;
    j++;
  }
  return pairs;
};

GrDiffGroup.prototype._updateRange = function(line) {
  if (line.beforeNumber === 'FILE' || line.afterNumber === 'FILE') { return; }

  if (line.type === GrDiffLine.Type.ADD ||
      line.type === GrDiffLine.Type.BOTH) {
    if (this.lineRange.right.start === null ||
        line.afterNumber < this.lineRange.right.start) {
      this.lineRange.right.start = line.afterNumber;
    }
    if (this.lineRange.right.end === null ||
        line.afterNumber > this.lineRange.right.end) {
      this.lineRange.right.end = line.afterNumber;
    }
  }

  if (line.type === GrDiffLine.Type.REMOVE ||
      line.type === GrDiffLine.Type.BOTH) {
    if (this.lineRange.left.start === null ||
        line.beforeNumber < this.lineRange.left.start) {
      this.lineRange.left.start = line.beforeNumber;
    }
    if (this.lineRange.left.end === null ||
        line.beforeNumber > this.lineRange.left.end) {
      this.lineRange.left.end = line.beforeNumber;
    }
  }
};
